use std::sync::Arc;

use axum::{
    extract::{Path, Query, State},
    Json,
};
use chrono::Local;
use validator::Validate;

use crate::{
    db, filter, form,
    handler::{get_conn, log_error, AddUrlResponse, AffResponse, BoolResponse, Response},
    hcaptcha,
    jwt::UserClaimsData,
    middleware::{ClientInfo, RequiredAuth},
    model, service, utils, AppState, Error, Result,
};

pub async fn visit(
    State(state): State<Arc<AppState>>,
    ci: ClientInfo,
    Path(url): Path<String>,
) -> Result<Json<Response<model::url::Url>>> {
    let handler_name = "web/url/visit";
    let pool = get_conn(&state);

    let url_data = service::url::visit(&pool, &url, &ci.ip, &ci.user_agent)
        .await
        .map_err(log_error(handler_name))?;

    Ok(Response::ok(url_data).to_json())
}

pub async fn visit_check_pwd(
    State(state): State<Arc<AppState>>,
    Json(frm): Json<form::url::VisitCheckPassword>,
) -> Result<Json<Response<BoolResponse>>> {
    let handler_name = "web/url/visit_check_pwd";
    let pool = get_conn(&state);

    let url = match service::url::find(&pool, &filter::url::FindBy::ID(&frm.id))
        .await
        .map_err(log_error(handler_name))?
    {
        Some(v) => v,
        None => return Err(Error::not_found("不存在的链接")),
    };

    if !utils::password::verify(&frm.password, &url.password).map_err(log_error(handler_name))? {
        return Err(Error::not_found("密码错误"));
    }

    Ok(Response::ok(BoolResponse { result: true }).to_json())
}

pub async fn add(
    State(state): State<Arc<AppState>>,
    RequiredAuth(claims): RequiredAuth<UserClaimsData>,
    Json(frm): Json<form::url::UserAdd>,
) -> Result<Json<Response<AddUrlResponse>>> {
    let handler_name = "web/url/add";

    frm.validate()
        .map_err(Error::from)
        .map_err(log_error(handler_name))?;

    if !hcaptcha::HCaptcha::from_cfg(&state.cfg.hcaptcha)
        .verify(&frm.captcha)
        .await
        .map_err(log_error(handler_name))?
    {
        return Err(Error::invalid_parameter("人机验证失败"));
    }

    let m = model::url::Url {
        user_id: claims.data.id,
        origin: frm.url.origin.clone(),
        has_password: frm.url.has_password(),
        password: frm.url.hashed_password().unwrap_or_default(),
        has_expired: frm.url.has_expired(),
        expired: frm.url.parsed_expired().unwrap_or_default(),
        dateline: Local::now(),
        ..Default::default()
    };

    let pool = get_conn(&state);

    let m = match service::url::grab_add(&pool, m, None)
        .await
        .map_err(log_error(handler_name))?
    {
        Some(v) => v,
        None => return Err(Error::internal_server("无法创建链接，请重试")),
    };

    Ok(Response::ok(AddUrlResponse { url: m.url }).to_json())
}

pub async fn list(
    State(state): State<Arc<AppState>>,
    RequiredAuth(claims): RequiredAuth<UserClaimsData>,
    Query(frm): Query<form::url::List>,
) -> Result<Json<Response<db::Pagination<model::url::Url>>>> {
    let handler_name = "web/url/list";
    let pool = get_conn(&state);

    frm.validate()
        .map_err(Error::from)
        .map_err(log_error(handler_name))?;

    let data = service::url::list(
        &pool,
        &filter::url::List {
            url: None,
            pagination: filter::Pagination {
                page: frm.p.page(),

                ..Default::default()
            },
            origin: None,
            has_password: None,
            has_expired: None,
            expired_range: None,
            order: None,
            user_id: Some(&claims.data.id),
        },
    )
    .await
    .map_err(log_error(handler_name))?;

    Ok(Response::ok(data).to_json())
}

pub async fn del(
    State(state): State<Arc<AppState>>,
    Path(id): Path<String>,
    RequiredAuth(claims): RequiredAuth<UserClaimsData>,
) -> Result<Json<Response<AffResponse>>> {
    let handler_name = "web/url/del";
    let pool = get_conn(&state);

    let rows = service::url::del(&pool, &id, Some(&claims.data.id))
        .await
        .map_err(log_error(handler_name))?;

    Ok(Response::ok(AffResponse { rows }).to_json())
}
